﻿using System;

using PostSharp.Aspects;
using BizElements.BusinessLayer;

namespace BizElements.BusinessLayer.Aspects
{
    #region Documentation.
    /// <summary>
    /// Marks that a method's return value is cached in <see cref="GlobalCache"/> or <see cref="UserCache"/>.
    /// </summary>
    /// <remarks><para>During the build process the code, which automatically caches and retrieves data from cache, is automatically injected
    /// and intercepts the method marked with <b>CachedAttribute</b> attribute.</para>
    /// <para>To use this attribute you have to install PostSharp 2 on your development computer and add references to <b>PostSharp.dll</b> assembly to your project.</para>
    /// </remarks>
    /// <example>The following example demonstrates how to use the attribute:
    /// <code>
    /// [Cached]
    /// public ICodeList FetchCodeList()
    /// {
    ///     return OrmUtil.FetchCodeList(_ConnectionProvider, this.Table.CountryId, this.Table.CountryName, null);
    /// }
    /// </code>
    /// The previous example is functionally identical to the following pattern:
    /// <code>
    /// public ICodeList FetchCodeList()
    /// {
    ///     // Check if requested data is already cached.
    ///     if (GlobalCache.Contains("CountrySearch.FetchCodeList"))
    ///         return GlobalCache.GetData("CountrySearch.FetchCodeList") as ICodeList;
    /// 
    ///     ICodeList codeList = OrmUtil.FetchCodeList(_ConnectionProvider, this.Table.CountryId, this.Table.CountryName, null);
    /// 
    ///     // Cache the data fetched from the data source. All data related to countries is placed in the same cache category ("Countries").
    ///     GlobalCache.SetData(/*cacheKey*/ "CountrySearch.FetchCodeList", codeList, /*category*/ "Countries");
    ///     return codeList;
    /// }
    /// </code>
    /// <para><b>RemoteableAttribute</b> and <b>CachedAttribute</b> can be used together. <b>CachedAttribute</b> has higher aspect priority, 
    /// thus it is evaluated before the <b>RemoteableAttribute</b>. The following example demonstrates how the attributes are combined:
    /// <code>
    /// [Remoteable]
    /// [Cached]
    /// public virtual ICodeList FetchCodeList()
    /// {
    ///     return OrmUtil.FetchCodeList(_ConnectionProvider, this.Table.CountryId, this.Table.CountryName, null);
    /// }
    /// </code>
    /// The previous example is functionally identical to the following pattern:
    /// <code>
    /// public ICodeList FetchCodeList()
    /// {
    ///     // Check if requested data is already cached in the current domain to avoid unnecessary remote method call.
    ///     if (GlobalCache.Contains("CountrySearch.FetchCodeList"))
    ///         return GlobalCache.GetData("CountrySearch.FetchCodeList") as ICodeList;
    /// 
    ///     ICodeList codeList = RpcHelper.InvokeMethod&lt;ICodeList&gt;(this, DbFetchCodeList);
    /// 
    ///     // Cache the data fetched from the data source in the current domain. 
    ///     // All data related to countries is placed in the same cache category ("Countries").
    ///     GlobalCache.SetData(/*cacheKey*/ "CountrySearch.FetchCodeList", codeList, /*category*/ "Countries");
    ///     return codeList;
    /// }
    /// 
    /// protected virtual ICodeList DbFetchCodeList()
    /// {
    ///     // Check if requested data is already cached in the remote domain/process to avoid unnecessary acces to database.
    ///     if (GlobalCache.Contains("CountrySearch.FetchCodeList"))
    ///         return GlobalCache.GetData("CountrySearch.FetchCodeList") as ICodeList;
    /// 
    ///     ICodeList codeList = OrmUtil.FetchCodeList(_ConnectionProvider, this.Table.CountryId, this.Table.CountryName, null);
    /// 
    ///     // Cache the data fetched from the data source in the remote domain. 
    ///     // All data related to countries is placed in the same cache category ("Countries").
    ///     GlobalCache.SetData(/*cacheKey*/ "CountrySearch.FetchCodeList", codeList, /*category*/ "Countries");
    ///     return codeList;
    /// }
    /// </code>
    /// </para>
    /// </example>
    #endregion
    [Serializable]
    [AttributeUsage(AttributeTargets.Method)]
    public sealed class CachedAttribute : OnMethodBoundaryAspect
    {
        #region CTor.

        /// <summary>
        /// Initializes a new instance of <b>CachedAttribute</b> class and sets its <b>AspectPriority</b>.
        /// </summary>
        public CachedAttribute()
        {
            // Highest possible priority to ensure that it always applied before Remoteable attribute, i.e. caching logic must be executed in all domains.
            this.AspectPriority = 1;
            this.VaryByParam = true;
            this.UserCacheAllowed = true;
        }

        #endregion

        #region OnMethodBoundaryAspect overrides.

        /// <summary>
        /// Checks if the requested data is cached. If it is then further method execution is stopped and the cached data is returned. Executed <b>before</b> the body of methods to which this aspect is applied. 
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed, which are its arguments, and how should the execution continue after the execution of <b>OnEntry</b>.</param>
        public sealed override void OnEntry(MethodExecutionArgs eventArgs)
        {
            string key = GetCacheKey(eventArgs);
            if (key == null)
                return;

            IActor user = GetActor(eventArgs);
            bool useUserCache = (this.UserCacheAllowed) && (user != null);
            bool isCached = (useUserCache) ? UserCache.Contains(user, key) : GlobalCache.Contains(key);
            if (isCached)
            {
                eventArgs.ReturnValue = (useUserCache) ? UserCache.GetData(user, key) : GlobalCache.GetData(key);
                eventArgs.FlowBehavior = FlowBehavior.Return;
                SetDontCacheOnExit(eventArgs, true);
            }
        }        

        /// <summary>
        /// Caches the method's return value if it wasn't already cached. Executed <b>after</b> the body of methods to which this aspect is applied. 
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed, which are its arguments, and how should the execution continue after the execution of <b>OnExit</b>.</param>
        public sealed override void OnExit(MethodExecutionArgs eventArgs)
        {
            bool dontCache = GetDontCacheOnExit(eventArgs);
            if (dontCache)
                return;

            string key = GetCacheKey(eventArgs);
            if (key == null)
                return;

            IActor user = GetActor(eventArgs);            
            int minutes = (this.ExpirationInMinutes > 0) ? this.ExpirationInMinutes : GlobalCache.DefaultAbsoluteExpirationInMinutes;
            string[] globalCategories = GetCategories(eventArgs);

            bool useUserCache = (this.UserCacheAllowed) && (user != null);
            if (useUserCache)
                UserCache.SetData(user, key, eventArgs.ReturnValue, minutes, this.SlidingExpiration, globalCategories);
            else
                GlobalCache.SetData(key, eventArgs.ReturnValue, minutes, this.SlidingExpiration, globalCategories);
        }

        private static void SetDontCacheOnExit(MethodExecutionArgs eventArgs, bool value)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            aopControl.Items["CachedAttribute.DontCacheOnExit"] = value;
        }

        private static bool GetDontCacheOnExit(MethodExecutionArgs eventArgs)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            bool dontCache = aopControl.Items.ContainsKey("CachedAttribute.DontCacheOnExit")
                ? Convert.ToBoolean(aopControl.Items["CachedAttribute.DontCacheOnExit"])
                : false;
            return dontCache;
        }

        private IActor GetActor(MethodExecutionArgs eventArgs)
        {
            return CacheAspectConfiguration.KeyGenerator.GetActor(eventArgs);
        }

        private string GetCacheKey(MethodExecutionArgs eventArgs)
        {
            string key;
            if (!string.IsNullOrEmpty(this.CacheKey))
                key = this.CacheKey;
            else
                key = CacheAspectConfiguration.KeyGenerator.GetCacheKey(eventArgs, this.IncludeActorInCacheKey, this.MutableParamsAllowed);

            return key;
        }

        private string[] GetCategories(MethodExecutionArgs eventArgs)
        {
            if (ArrayUtil.IsNullOrEmpty(this.Categories))
                return CacheAspectConfiguration.CategoryGenerator.GetCategories(eventArgs);
            else
                return this.Categories;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets manually defined cache key at which the value is stored.
        /// </summary>
        /// <value>Cache key string. Default is <b>null</b> indicating that the key will be automatically created based on the method and class name.</value>
        public string CacheKey { get; set; }

        /// <summary>
        /// Gets or sets manually defined global cache categories to which the cache item belongs to.
        /// </summary>
        /// <value>Array of strings. Default is <b>null</b> indicating that the categories will be automatically assigned using the configured <see cref="ICacheCategoryGenerator"/> object.</value>
        public string[] Categories { get; set; }

        /// <summary>
        /// Gets or sets value indicating whether sliding or absolute time expiration schema is used for this item.
        /// </summary>
        /// <value><b>true</b> to use sliding expiration; <b>false</b> to use absolute expiration. Default is <b>false</b>.</value>
        public bool SlidingExpiration { get; set; }

        /// <summary>
        /// Gets or sets time in minutes after which the cache item will expire.
        /// </summary>
        /// <value>An integer greater or equal to 1. If not set the <see cref="GlobalCache.DefaultAbsoluteExpirationInMinutes"/> value is used.</value>
        public int ExpirationInMinutes { get; set; }

        /// <summary>
        /// Gets or sets whether cache key is automatically generated depending on method arguments.
        /// </summary>
        /// <value><b>true</b> to generate unique cache keys based on method argument; <b>false</b> otherwise. Default is <b>true</b>.</value>
        public bool VaryByParam { get; set; }

        /// <summary>
        /// Gets or sets whether cache key can be generated for mutable objects.
        /// </summary>
        /// <value><b>true</b> to generate unique cache keys if method arguments contain mutable objects; <b>false</b> otherwise.
        /// Default is <b>false</b> indicating that no caching occurs if method uses mutable objects as arguments.
        /// The only exception are <see cref="IActor"/> objects when <see cref="UserCache"/> is used.</value>
        public bool MutableParamsAllowed { get; set; }

        /// <summary>
        /// Gets or sets whether <see cref="IActor"/> method arguments are included when <see cref="VaryByParam"/> is set to <b>true</b>.
        /// </summary>
        /// <value><b>true</b> to include <b>IActor</b> data in unique key; <b>false</b> otherwise. Default is <b>false</b>.</value>
        public bool IncludeActorInCacheKey { get; set; }

        /// <summary>
        /// Gets or sets whether <see cref="UserCache"/> can be used.
        /// </summary>
        /// <value><b>true</b> to use <b>UserCache</b> if <b>IActor</b> object can be retrieved by <see cref="ICacheKeyGenerator"/>
        /// object; <b>false</b> to force <b>GlobalCache</b> usage. Default is <b>true</b>.</value>
        public bool UserCacheAllowed { get; set; }

        #endregion
    }
}
